package email;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.Location;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;

public class DBController {

	private File db;
	private final String DB_DIR = "storage";
	private final String DB_FILE = "message_db.xml";

	public DBController(String path) throws FileNotFoundException,
			XMLStreamException, FactoryConfigurationError {
		String dbPath = path;
		File dir = new File(path);
		if (!dir.isDirectory()) {

		}

		if (!dbPath.endsWith("/"))
			dbPath += "/";

		dbPath += DB_DIR;
		init(dbPath);
	}

	private void init(String dbPath) throws FileNotFoundException,
			XMLStreamException, FactoryConfigurationError {

		File dbDir = new File(dbPath);
		if (!dbDir.exists()) {
			dbDir.mkdirs();
		}

		String dbFileName = dbPath += "/" + DB_FILE;
		db = new File(dbFileName);
		if (!db.exists()) {
			try {
				db.createNewFile();
				initDB();
			} catch (IOException e) {
			}
		}
	}

	private void initDB() throws FileNotFoundException, XMLStreamException,
			FactoryConfigurationError {

		FileOutputStream outStream = new FileOutputStream(db);

		XMLStreamWriter writer = XMLOutputFactory.newInstance()
				.createXMLStreamWriter(outStream);

		writer.writeStartDocument();

		writer.writeStartElement("Database");

		writer.writeStartElement("Messages");
		writer.writeStartElement("maxid");
		writer.writeCharacters("1");
		writer.writeEndElement();
		writer.writeStartElement("Message");
		writer.writeStartElement("id");
		writer.writeCharacters("0");
		writer.writeEndElement();
		writer.writeStartElement("RecvdTime");
		writer.writeCharacters("0");
		writer.writeEndElement();
		writer.writeEndElement();
		writer.writeEndElement();

		writer.writeStartElement("Threads");
		writer.writeStartElement("maxid");
		writer.writeCharacters("1");
		writer.writeEndElement();
		writer.writeStartElement("Thread");
		writer.writeStartElement("id");
		writer.writeCharacters("0");
		writer.writeEndElement();
		writer.writeStartElement("RecvdTime");
		writer.writeCharacters("0");
		writer.writeEndElement();
		writer.writeEndElement();
		writer.writeEndElement();
		writer.writeEndElement();

		writer.writeEndDocument();

		writer.close();
	}

	/**
	 * Gets all Threads
	 * 
	 * @return
	 */
	public List<Thread> getAllThreads() {
		return selectThread(new HashMap<String, String>());
	}

	public ReceivedMessage getMessage(long id) {
		return null;
	}

	public Thread getThread(long id) {
		return null;
	}

	public long addMessage(ReceivedMessage m) throws FileNotFoundException,
			XMLStreamException, FactoryConfigurationError {

		this.insert(m);
		return -1;
	}

	/**
	 * Inserts into the table specified with the specified attributes
	 * 
	 * The insert is kinda loose, it's not really adhering to a specific schema,
	 * and will insert whatever key/value pair you specify.
	 * 
	 * Select will probably work the same way.
	 * 
	 * DBTest.java includes a nice example of how to use this method along with
	 * construction and whatnot
	 * 
	 * @param table
	 * @param namespace
	 *            --The actual name of the element your inserting (i.e., Thread,
	 *            Message, etc.)
	 * 
	 * @param attrs
	 *            A map where key is the name of the attribute and value is the
	 *            value of that attribute. For example, you want
	 *            <RecvdTime>999</RecvdTime><From>YourMother</From>, then the
	 *            map will look like {"RecvdTime"=>"999", "From"=>"YourMother"}
	 * 
	 *            Haven't found a good way to handle these yet, deal for the
	 *            time being
	 * @throws XMLStreamException
	 * @throws IOException
	 */
	public boolean insert(String table, String namespace,
			Map<String, String> attrs, String orderByAttr)

	throws XMLStreamException, IOException {

		InputStream stream = new FileInputStream(db);
		XMLInputFactory f = XMLInputFactory.newInstance();
		XMLStreamReader read = f.createXMLStreamReader(stream);
		boolean ignoreId = false;
		boolean inTable = false;
		boolean inElement = false;
		boolean completeMatch = true;
		Location prevLoc = null;
		Location elementLoc = null;
		Location idStartLoc = null;
		Location idEndLoc = null;

		ignoreId = attrs.keySet().contains("id");

		String maxTag = "maxid";

		long id = -1;

		while (read.hasNext()) {
			read.next();
			try {
				if (read.isStartElement()) {
					String name = read.getLocalName();
					if (name.equals(table)) {
						inTable = true;
						elementLoc = read.getLocation();
					} else if (inTable && name.equals(namespace)) {
						inElement = true;
						prevLoc = elementLoc;
					} else if (inElement && name.equals(orderByAttr)) {
						try {
							long time = Long.parseLong(read.getElementText());
							if (Long.parseLong(attrs.get(orderByAttr)) < time) {
								elementLoc = prevLoc;
								break;
							} else {
								prevLoc = elementLoc;
								elementLoc = read.getLocation();
							}
						} catch (Exception e) {
							// There was an error parsing one or both of those
							// longs, skip it I guess.
							prevLoc = elementLoc;
							elementLoc = read.getLocation();
						}

						// if attr[time] less than this time place before, set
						// loc to prevLoc and break
						// else goto next element, set prevLoc to loc and loc to
						// current location [after end_el]
					} else if (inTable && name.equals(maxTag) && !ignoreId) {
						idStartLoc = read.getLocation();
						id = Long.parseLong(read.getElementText());
						idEndLoc = read.getLocation();
					}

				} else if (read.isEndElement()) {
					String name = read.getLocalName();
					if (name.equals(table)) {
						inTable = false;
					} else if (name.equals(namespace)) {
						inElement = false;
						prevLoc = elementLoc;
						elementLoc = read.getLocation();
					}
					continue;
				}
			} catch (Exception e) {
				return false;
			}
		}

		if (!ignoreId)
			id++;

		// Write the elements with a RandonAccessFile
		return writeElement(namespace, attrs, elementLoc, idStartLoc, idEndLoc,
				id);
	}

	private void printStuff(Location loc) {
		try {
			RandomAccessFile file = new RandomAccessFile(db, "r");
			file.seek(loc.getCharacterOffset());
		} catch (Exception e) {
		}
	}

	private boolean writeElement(String namespace, Map<String, String> attrs,
			Location startLoc, Location endLoc, Location idStartLoc,
			Location idEndLoc, long id) {
		try {
			RandomAccessFile file = new RandomAccessFile(db, "rw");
			
			String line = "";
			String end = "";
			
			file.seek((long) endLoc.getCharacterOffset());
			
			line = file.readLine();
			while (line != null) {
				end += line;
				line = file.readLine();
			}
			System.out.println(end);

			file.seek((long) startLoc.getCharacterOffset());

			String startElement = "<" + namespace + ">";
			String endElement = "</" + namespace + ">";

			file.writeBytes(startElement);
			if (id >= 0) {
				file.writeBytes("<id>");
				file.writeBytes("" + id);
				file.writeBytes("</id>");
			}
			for (String attribute : attrs.keySet()) {
				String startAttr = "<" + attribute + ">";
				String value = attrs.get(attribute);
				String endAttr = "</" + attribute + ">";

				file.writeBytes(startAttr);
				file.writeBytes(value);
				file.writeBytes(endAttr);
			}
			file.writeBytes(endElement);
			file.writeBytes(end);
			
			if (id >= 0) {
				file.seek((long) idEndLoc.getCharacterOffset());
				line = "";
				end = "";
				line = file.readLine();
				while (line != null) {
					end += line;
					line = file.readLine();
				}
				file.seek((long) idStartLoc.getCharacterOffset());
				file.writeBytes("" + id);
				file.writeBytes("</maxid>");
				file.writeBytes(end);
			}
			file.setLength(file.getFilePointer());
			file.close();
		} catch (Exception e) {
			return false;
		}
		return true;
	}

	private boolean writeElement(String namespace, Map<String, String> attrs,
			Location elementLoc, Location idStartLoc, Location idEndLoc, long id)
			throws IOException {
		return writeElement(namespace, attrs, elementLoc, elementLoc, idStartLoc, idEndLoc, id);
	}

	public void update(long id, Thread t) {
		Map<String, String> attrs = getAttrsForThread(t);
		update("Threads", "Thread", attrs, id);
	}
	
	private Map<String, String> getAttrsForThread(Thread t) {
		Map<String, String> ret = new HashMap<String, String>();
		
		String messages = "";
		Date recvdTime = new Date(0); //Jan 1, 1970 00:00:00
		for (ReceivedMessage m : t.getEmails()) {
			messages += m.getId() + " ";
			if (recvdTime.getTime() < m.getDateDate().getTime()) {
				recvdTime = m.getDateDate();
			}
		}
		
		ret.put("id", t.getId() + "");
		ret.put("Name", t.getName());
		ret.put("ThreadMessages", messages);
		ret.put("RecvdTime", recvdTime.getTime() + "");
		
		return ret;
	}
	
	private Map<String, String> getAttrsForMessage(ReceivedMessage m) {
		Map<String, String> ret = new HashMap<String, String>();
		
		ret.put("id", m.getId() + "");
		ret.put("Subject", m.getSubject());
		ret.put("From", m.getFrom());
		ret.put("RecvdTime", m.getDateDate().getTime() + "");
		ret.put("Recipients", m.getRecipients());
		ret.put("Body", m.getBody());
		
		return ret;
	}

	private void update(String table, String namespace,
			Map<String, String> attrs, long id) {
		try {
			InputStream stream = new FileInputStream(db);
			XMLInputFactory f = XMLInputFactory.newInstance();
			XMLStreamReader read = f.createXMLStreamReader(stream);
			boolean inElement = false;
			boolean inTable = false;
			Location prevLoc = null;
			Location elementLoc = null;
			boolean foundEl = false;

			while (read.hasNext()) {
				read.next();
				if (read.isStartElement()) {
					String name = read.getLocalName();
					if (name.equals(table)) {
						inTable = true;
						elementLoc = read.getLocation();
					} else if (inTable && name.equals(namespace)) {
						inElement = true;
						prevLoc = elementLoc;
					} else if (inElement && name.equals("id")) {
						long elId = Long.parseLong(read.getElementText());
						if (elId == id) {
							foundEl = true;
						}
					}
				} else if (read.isEndElement()) {
					String name = read.getLocalName();
					if (name.equals(table)) {
						inTable = false;
					} else if (name.equals(namespace)) {
						inElement = false;
						if (foundEl) {
							elementLoc = read.getLocation();
							writeElement(namespace, attrs, prevLoc, elementLoc, null, null, -1);
							return;
						}
						prevLoc = elementLoc;
						elementLoc = read.getLocation();
					}
				}
			}
			
		} catch (Exception e) {
			return;
		}
	}

	/*
	 * For logistical purposes, I need the thread to be passed in as well.
	 * I will ignore the name of the passed in Thread however.
	 */
	public void rename(long id, Thread t, String name) {
		Map<String, String> attrs = getAttrsForThread(t);
		attrs.put("Name", name);
		update("Threads", "Thread", attrs, id);
	}

	public boolean insert(Thread t) {
		String table = "Threads";
		String namespace = "Thread";

		Map<String, String> attrs = new HashMap<String, String>();
		attrs.put("id", t.getId() + "");
		List<Thread> threads = selectThread(attrs);

		if (threads.size() > 0) {
			return false;
		}

		String idVal = t.getId() + "";
		String titleVal = t.getName();
		String messages = "";
		long recvdTime = -1;
		for (ReceivedMessage m : t.getEmails()) {
			insert(m);
			messages += m.getId() + " ";
			DateFormat fmt = new SimpleDateFormat(
					"EEE MMM dd HH:mm:ss zzzz yyyy");
			Date d = new Date();
			try {
				d = fmt.parse(m.getDate());
			} catch (ParseException e) {
				// ignore it. Set recvdTime to our recvdTime.
			}
			if (d.getTime() > recvdTime)
				recvdTime = d.getTime();
		}

		attrs = getAttrsForThread(t);
		attrs.put("RecvdTime", recvdTime + "");

		try {
			return this.insert(table, namespace, attrs, "RecvdTime");
		} catch (Exception e) {
			return false;
		}
	}

	public boolean insert(ReceivedMessage m) {
		String table = "Messages";
		String namespace = "Message";
		Map<String, String> attrs = new HashMap<String, String>();

		attrs.put("id", m.getId() + "");
		List<ReceivedMessage> result = selectMessage(attrs);
		if (result.size() > 0) {
			return false;
		}

		Date d = m.getDateDate();
		String date = "";
		if (d == null) {
			date = m.getDate();
		} else {
			date = "" + m.getDateDate().getTime();
		}

		attrs = getAttrsForMessage(m);

		try {
			return this.insert(table, namespace, attrs, "RecvdTime");
		} catch (Exception e) {
			return false;
		}
	}

	/**
	 * Everything is pretty straight forward for the attributes except for
	 * Messages. Their ids should be put into a single whitespace delimited
	 * string.
	 * 
	 * @param attrs
	 * @return
	 * 
	 *         TODO -- TEST
	 */
	public List<Thread> selectThread(Map<String, String> attrs) {

		ArrayList<Thread> ret = new ArrayList<Thread>();
		String table = "Threads";
		String namespace = "Thread";
		Map<Thread, String> threadMap = new HashMap<Thread, String>();

		try {
			InputStream stream = new FileInputStream(db);
			XMLInputFactory f = XMLInputFactory.newInstance();
			XMLStreamReader read = f.createXMLStreamReader(stream);

			boolean inTable = false;
			boolean inElement = false;
			boolean cleanElement = false;
			Thread tmp = null;

			while (read.hasNext()) {
				read.next();

				if (read.isStartElement()) {
					String name = read.getLocalName();
					if (name.equals(table)) {
						inTable = true;
					} else if (inTable && name.equals(namespace)) {
						inElement = true;
						cleanElement = true;
						tmp = new Thread();
						threadMap.put(tmp, "");
					} else if (inElement) {
						String value = read.getElementText();
						for (String attr : attrs.keySet()) {
							if (name.equals(attr)) {
								//System.out.println(name);
								//System.out.println(value + " == " + attrs.get(attr));
								if (!(attrs.get(attr).equals(value))) {
									cleanElement = false;
									threadMap.remove(tmp);
									ret.remove(tmp);
								} else {

								}
								break;
							}
						}
						if (name.equals("id")) {
							tmp.setId(Integer.parseInt(value));
						} else if (name.equals("Name")) {
							tmp.setName(value);
						} else if (name.equals("ThreadMessages")) {
							threadMap.put(tmp, value);
						}
					}
				} else if (read.isEndElement()) {
					String name = read.getLocalName();
					if (name.equals(table)) {
						inTable = false;
						inElement = false;
					} else if (inTable && name.equals(namespace)) {
						inElement = false;
						if (cleanElement) {
							threadMap.put(tmp, threadMap.get(tmp));
							ret.add(tmp);
						}
					}
				}
			}
			Map<Long, ReceivedMessage> messageMap = new HashMap<Long, ReceivedMessage>();
			Map<String, String> messageAttrs = new HashMap<String, String>();
			for (Thread t : threadMap.keySet()) {
				String[] messages = threadMap.get(t).split(" ");
				for (String s : messages) {
					if (s.isEmpty()) {
						continue;
					}

					ReceivedMessage email;

					email = messageMap.get(Long.parseLong(s));
					if (email == null) {
						messageAttrs.put("id", s);
						List<ReceivedMessage> emailList = selectMessage(messageAttrs);
						if (emailList.size() == 0) {
							continue;
						}
						email = emailList.get(0);
						if (email != null)
							messageMap.put(email.getId(), email);
					}
					t.addEmail(email);
				}
			}
			// Should have all the elements we're gonna be using
		} catch (Exception e) {
			return new ArrayList<Thread>();
		}

		return ret;
	}

	// TODO -- TEST.. Seems to be working
	public List<ReceivedMessage> selectMessage(Map<String, String> attrs) {
		ArrayList<ReceivedMessage> ret = new ArrayList<ReceivedMessage>();
		String table = "Messages";
		String namespace = "Message";

		try {
			InputStream stream = new FileInputStream(db);
			XMLInputFactory f = XMLInputFactory.newInstance();
			XMLStreamReader read = f.createXMLStreamReader(stream);

			boolean inTable = false;
			boolean inElement = false;
			boolean cleanElement = false;
			ReceivedMessage tmp = null;

			while (read.hasNext()) {
				read.next();

				if (read.isStartElement()) {
					String name = read.getLocalName();
					if (name.equals(table)) {
						inTable = true;
					} else if (inTable && name.equals(namespace)) {
						inElement = true;
						cleanElement = true;
						tmp = new ReceivedMessage();
						ret.add(tmp);
					} else if (inElement) {
						String value = read.getElementText();
						for (String attr : attrs.keySet()) {
							if (name.equals(attr)) {
								if (!attrs.get(attr).equals(value)) {
									cleanElement = false;
									ret.remove(tmp);
								} else {

								}
								break;
							}
						}
						if (name.equals("id")) {
							tmp.setId(Long.parseLong(value));
						} else if (name.equals("Subject")) {
							tmp.setSubject(value);
						} else if (name.equals("From")) {
							tmp.setFrom(value);
						} else if (name.equals("RecvdTime")) {
							long epoch = Long.parseLong(value);
							tmp.setDate(new Date(epoch));
						} else if (name.equals("Recipients")) {
							tmp.setRecipients(value);
						} else if (name.equals("Body")) {
							tmp.setBody(value);
						}
					}
				} else if (read.isEndElement()) {
					String name = read.getLocalName();
					if (name.equals(table)) {
						inTable = false;
						inElement = false;
					} else if (inTable && name.equals(namespace)) {
						inElement = false;
						if (cleanElement) {
							ret.add(tmp);
						}
					}
				}
			}
			// Should have all the elements we're gonna be using
		} catch (Exception e) {
			return new ArrayList<ReceivedMessage>();
		}

		return ret;
	}

	public long addThread(Thread t) {
		this.insert(t);
		return 0;
	}

	// Don't think I need this...
	public ReceivedMessage[] getMessagesFromThread(long id) {

		return null;
	}

	public Thread mergeThread(Thread[] thr) {
		String name = "Untitled Thread";
		return mergeThread(thr, name);

	}

	// TODO -- TEST
	public Thread mergeThread(Thread[] threads, String name) {
		Thread ret = new Thread();

		for (Thread t : threads) {
			ArrayList<ReceivedMessage> messages = t.getEmails();
			for (ReceivedMessage m : messages) {
				if (!ret.getEmails().contains(m)) {
					ret.addEmail(m);
				}
			}
		}
		ret.setName(name);
		ret.setId(ret.hashCode());
		if (insert(ret)) {
			// delete all the other threads
			for (Thread t : threads) {
				unthread(t);
			}
			return ret;
		}
		return null;
	}

	public void mergeThread(long[] merged, Thread newThread) {
		for (long id : merged) {
			delete("Threads", "Thread", id);
		}
		addThread(newThread);
	}

	public void unthread(Thread t) {
		delete("Threads", "Thread", t.getId());
	}

	public void deleteMessage(ReceivedMessage m) {
		delete("Messages", "Message", m.getId());
	}

	private void delete(String table, String namespace, long id) {
		try {
			InputStream stream = new FileInputStream(db);
			XMLInputFactory f = XMLInputFactory.newInstance();
			XMLStreamReader read = f.createXMLStreamReader(stream);
			boolean inTable = false;
			boolean inElement = false;
			Location startLoc = null;
			Location endLoc = null;

			boolean foundElement = false;

			while (read.hasNext()) {
				read.next();
				if (read.isStartElement()) {
					String name = read.getLocalName();
					if (name.equals(table)) {
						inTable = true;
						startLoc = read.getLocation();
					} else if (inTable && name.equals(namespace)) {
						inElement = true;
					} else if (inElement && name.equals("id")) {
						try {
							if (Long.parseLong(read.getElementText()) == id) {
								foundElement = true;
							}
						} catch (Exception e) {
						}
					}

				} else if (read.isEndElement()) {
					String name = read.getLocalName();
					if (name.equals(table)) {
						inTable = false;
					} else if (name.equals(namespace)) {
						inElement = false;
						if (foundElement) {
							endLoc = read.getLocation();
							break;
						} else {
							startLoc = read.getLocation();
						}
					}
					continue;
				}
			}
			if (foundElement) {
				deleteElement(startLoc, endLoc);
			}
		} catch (Exception e) {
			return;
		}
	}

	// TODO -- TEST
	private void deleteElement(Location startLoc, Location endLoc) {
		try {
			RandomAccessFile file = new RandomAccessFile(db, "rw");
			file.seek((long) endLoc.getCharacterOffset());

			String line = "";
			String end = "";
			line = file.readLine();
			while (line != null) {
				end += line;
				line = file.readLine();
			}

			file.seek((long) startLoc.getCharacterOffset());

			file.writeBytes(end);
			file.setLength(file.getFilePointer());
			file.close();
		} catch (Exception e) {
			return;
		}
	}
}